// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © blackcat1402
//@version=6
indicator(title = "[blackcat] L1 Fast Scalp Profiter", shorttitle = "L1FSP", overlay = false, max_labels_count = 500)

// ————— Inputs: oscillator core (every V1 constant exposed) —————
const string GRP_CORE = "Oscillator Core"
stochLenInput = input.int(30, "Stochastic Lookback (d / j)", minval = 5, maxval = 200, group = GRP_CORE)
slowLenInput = input.int(20, "Slow EMA (d)", minval = 2, maxval = 200, group = GRP_CORE)
fastLenInput = input.int(5, "Fast EMA (j)", minval = 1, maxval = 100, group = GRP_CORE)
kSmoothLenInput = input.int(3, "Signal Smoothing (k of d)", minval = 1, maxval = 50, group = GRP_CORE)
mSmoothLenInput = input.int(7, "Signal Smoothing (m of j)", minval = 1, maxval = 50, group = GRP_CORE)
biasFastLenInput = input.int(6, "Bias Fast SMA", minval = 1, maxval = 100, group = GRP_CORE)
biasMidLenInput = input.int(12, "Bias Mid SMA", minval = 2, maxval = 200, group = GRP_CORE)
biasSlowLenInput = input.int(24, "Bias Slow SMA", minval = 3, maxval = 400, group = GRP_CORE)
biasW1Input = input.float(1.0, "Bias Weight Fast", minval = 0.0, maxval = 10.0, step = 0.5, group = GRP_CORE)
biasW2Input = input.float(2.0, "Bias Weight Mid", minval = 0.0, maxval = 10.0, step = 0.5, group = GRP_CORE)
biasW3Input = input.float(3.0, "Bias Weight Slow", minval = 0.0, maxval = 10.0, step = 0.5, group = GRP_CORE)
biasSmoothLenInput = input.int(3, "Bias Smoothing (mn)", minval = 1, maxval = 50, group = GRP_CORE)
powerLowLenInput = input.int(36, "Power Lowest-Low Lookback", minval = 5, maxval = 300, group = GRP_CORE)
powerHighLenInput = input.int(30, "Power Highest-High Lookback", minval = 5, maxval = 300, group = GRP_CORE)
powerSmoothLenInput = input.int(4, "Power EMA (var26)", minval = 1, maxval = 50, group = GRP_CORE)
oversoldInput = input.float(10.0, "Oversold Level", minval = 0.0, maxval = 49.0, step = 1.0, group = GRP_CORE)
overboughtInput = input.float(90.0, "Overbought Level", minval = 51.0, maxval = 100.0, step = 1.0, group = GRP_CORE)
biasTriggerInput = input.float(-4.0, "Bias Watch Trigger (dianguancha)", maxval = 0.0, step = 0.5, group = GRP_CORE)
diboliOnInput = input.float(90.0, "Bottom-Power Line (active)", minval = 0.0, maxval = 100.0, step = 1.0, group = GRP_CORE)
diboliOffInput = input.float(100.0, "Bottom-Power Line (idle)", minval = 0.0, maxval = 100.0, step = 1.0, group = GRP_CORE)
dingboliOnInput = input.float(5.0, "Top-Power Line (active)", minval = 0.0, maxval = 100.0, step = 1.0, group = GRP_CORE)
dingboliOffInput = input.float(18.0, "Top-Power Line (idle)", minval = 0.0, maxval = 100.0, step = 1.0, group = GRP_CORE)

// ————— Inputs: signal rules —————
const string GRP_SIG = "Signal Rules"
enableLongInput = input.bool(true, "Enable Long Entries", group = GRP_SIG)
enableShortInput = input.bool(true, "Enable Short Entries", group = GRP_SIG)
useZoneFilterInput = input.bool(true, "Filter Entries by Power Zone", group = GRP_SIG)
zoneLongMaxInput = input.float(35.0, "Long Entry Power Ceiling", minval = 1.0, maxval = 99.0, step = 1.0, group = GRP_SIG)
zoneShortMinInput = input.float(65.0, "Short Entry Power Floor", minval = 1.0, maxval = 99.0, step = 1.0, group = GRP_SIG)
useBiasGateInput = input.bool(true, "Filter Entries by Bias Sign", group = GRP_SIG)
biasLongGateInput = input.float(0.0, "Long Entry Bias Ceiling", maxval = 20.0, step = 0.5, group = GRP_SIG)
biasShortGateInput = input.float(0.0, "Short Entry Bias Floor", minval = -20.0, step = 0.5, group = GRP_SIG)
usePowerEntryInput = input.bool(false, "Add Power Recovery Entries", group = GRP_SIG)
useReverseExitInput = input.bool(true, "Exit on Reverse Cross", group = GRP_SIG)
usePowerExitInput = input.bool(true, "Exit on Power Zone Rejection", group = GRP_SIG)
useBiasExitInput = input.bool(false, "Exit on Bias Zero Cross", group = GRP_SIG)

// ————— Inputs: signal governance —————
const string GRP_GOV = "Signal Governance"
useExclusivityInput = input.bool(true, "Same-bar Entry / Exit Exclusivity", group = GRP_GOV)
cooldownBarsInput = input.int(3, "Entry Cooldown Bars", minval = 0, maxval = 200, group = GRP_GOV)

// ————— Inputs: risk management —————
const string GRP_RISK = "Risk Management"
useTpslInput = input.bool(true, "Compute ATR Stop / Target", group = GRP_RISK)
atrLenInput = input.int(14, "ATR Length", minval = 1, maxval = 200, group = GRP_RISK)
slMultInput = input.float(2.0, "Stop Loss (xATR)", minval = 0.5, maxval = 20.0, step = 0.25, group = GRP_RISK)
tpMultInput = input.float(3.0, "Take Profit (xATR)", minval = 0.5, maxval = 50.0, step = 0.25, group = GRP_RISK)

// ————— Inputs: re-entry —————
const string GRP_RE = "Re-entry"
useReentryInput = input.bool(false, "Enable Re-entry after Exit", group = GRP_RE)
reentryWinInput = input.int(1, "Anchor Window at Exit Bar", minval = 1, maxval = 20, group = GRP_RE)
reentryModeInput = input.string("Close", "Breakout Source", options = ["Close", "High/Low"], group = GRP_RE)

// ————— Inputs: HTF trend gate —————
const string GRP_HTF = "HTF Trend Gate"
useMtfInput = input.bool(false, "Enable HTF Direction Gate", group = GRP_HTF)
htfTimeframeInput = input.timeframe("240", "HTF Timeframe", group = GRP_HTF)
htfEmaLenInput = input.int(50, "HTF EMA Length", minval = 1, maxval = 500, group = GRP_HTF)

// ————— Inputs: visuals —————
const string GRP_VIS = "Visuals"
showLabelsInput = input.bool(true, "Show Signal Labels", group = GRP_VIS)
showLegacyPlotshapeInput = input.bool(false, "Show Legacy plotshape Markers", group = GRP_VIS)
showWatchMarkerInput = input.bool(true, "Show Bias Watch Marker", group = GRP_VIS)
showPowerLinesInput = input.bool(true, "Show Bottom / Top Power Lines", group = GRP_VIS)
showBgStateInput = input.bool(true, "Show Position Background", group = GRP_VIS)
showDashboardInput = input.bool(true, "Show Dashboard", group = GRP_VIS)
slowColorInput = input.color(color.green, "Slow Line (k)", group = GRP_VIS)
midColorInput = input.color(color.white, "Mid Line (m)", group = GRP_VIS)
fastColorInput = input.color(color.red, "Fast Line (j)", group = GRP_VIS)
watchColorInput = input.color(color.blue, "Bias Watch", group = GRP_VIS)
powerColorInput = input.color(color.yellow, "Power Lines", group = GRP_VIS)
textColorInput = input.color(color.white, "Marker Text", group = GRP_VIS)

// ————— Color semantics —————
color C_LONG = color.new(color.green, 0)
color C_SHORT = color.new(color.red, 0)
color C_EXIT = color.new(color.orange, 0)
color C_RE = color.new(color.teal, 0)
color C_RE_SHORT = color.new(color.maroon, 0)
color C_WATCH = watchColorInput
color C_TEXT = textColorInput
color C_NONE = color.new(color.white, 100)
color C_BGLONG = color.new(color.green, 94)
color C_BGSHORT = color.new(color.red, 94)

// ————— Calculations: oscillator core (V1 algorithm unchanged, divisions guarded) —————
float stochHi = ta.highest(high, stochLenInput)
float stochLo = ta.lowest(low, stochLenInput)
float stochRng = stochHi - stochLo
float stochRaw = stochRng > 0 ? (close - stochLo) / stochRng * 100 : 50.0
float d = ta.ema(stochRaw, slowLenInput)
float j = ta.ema(stochRaw, fastLenInput)
float biasFastSma = ta.sma(close, biasFastLenInput)
float biasMidSma = ta.sma(close, biasMidLenInput)
float biasSlowSma = ta.sma(close, biasSlowLenInput)
float bias1w = biasFastSma != 0 ? (close - biasFastSma) / biasFastSma * 100 : 0.0
float bias2w = biasMidSma != 0 ? (close - biasMidSma) / biasMidSma * 100 : 0.0
float bias3w = biasSlowSma != 0 ? (close - biasSlowSma) / biasSlowSma * 100 : 0.0
float biasWeight = biasW1Input + biasW2Input + biasW3Input
float mm = biasWeight > 0 ? (biasW1Input * bias1w + biasW2Input * bias2w + biasW3Input * bias3w) / biasWeight : 0.0
float mn = ta.sma(mm, biasSmoothLenInput)
float powerLo = ta.lowest(low, powerLowLenInput)
float powerHi = ta.highest(high, powerHighLenInput)
float powerRng = powerHi - powerLo
float powerRaw = powerRng > 0 ? (close - powerLo) / powerRng * 100 : 50.0
float var26 = ta.ema(powerRaw, powerSmoothLenInput)
float k = ta.ema(d, kSmoothLenInput)
float m = ta.ema(j, mSmoothLenInput)
float gujing = j
float dianguancha = mn < biasTriggerInput ? 10 : 0
float diboli = var26 < oversoldInput ? diboliOnInput : diboliOffInput
float dingboli = var26 > overboughtInput ? dingboliOnInput : dingboliOffInput

// ————— Visuals: core lines (V1 plots preserved, titled) —————
pK = plot(k, "Slow Line (k)", color = slowColorInput, linewidth = 2)
pM = plot(m, "Mid Line (m)", color = midColorInput, linewidth = 1)
pJ = plot(gujing, "Fast Line (j)", color = fastColorInput, linewidth = 1)
fill(pK, pM, color = j > d ? color.new(color.green, 82) : color.new(color.red, 82))
hline(oversoldInput, "Oversold", color = color.new(color.gray, 60), linestyle = hline.style_dotted)
hline(50.0, "Midline", color = color.new(color.gray, 70), linestyle = hline.style_dashed)
hline(overboughtInput, "Overbought", color = color.new(color.gray, 60), linestyle = hline.style_dotted)
plot(showPowerLinesInput ? diboli : na, "Bottom-Power Line", color = powerColorInput, style = plot.style_stepline, linewidth = 1)
plot(showPowerLinesInput ? dingboli : na, "Top-Power Line", color = color.new(color.white, 40), style = plot.style_stepline, linewidth = 1)
plot(dianguancha, "Bias Watch Value (10 = watching)", display = display.data_window)
plot(var26, "Power Oscillator (var26)", display = display.data_window)
plot(mn, "Weighted Bias (mn)", display = display.data_window)

// ————— Calculations: triggers (edge based, replaces V1 level logic) —————
bool longCross = ta.crossover(j, d)
bool shortCross = ta.crossunder(j, d)
bool powerUpFromOversold = ta.crossover(var26, oversoldInput)
bool powerDownFromOverbought = ta.crossunder(var26, overboughtInput)
bool biasDownZero = ta.crossunder(mn, 0.0)
bool biasUpZero = ta.crossover(mn, 0.0)
bool watchOn = mn < biasTriggerInput
bool watchEdge = watchOn and not watchOn[1]

bool zoneLongOK = not useZoneFilterInput or var26 < zoneLongMaxInput
bool zoneShortOK = not useZoneFilterInput or var26 > zoneShortMinInput
bool biasLongOK = not useBiasGateInput or mn < biasLongGateInput
bool biasShortOK = not useBiasGateInput or mn > biasShortGateInput

bool longSignalRaw = (longCross and zoneLongOK and biasLongOK) or (usePowerEntryInput and powerUpFromOversold and j > k)
bool shortSignalRaw = (shortCross and zoneShortOK and biasShortOK) or (usePowerEntryInput and powerDownFromOverbought and j < k)

// ————— Calculations: HTF trend gate (zero repaint) —————
bool htfValid = timeframe.in_seconds(htfTimeframeInput) > timeframe.in_seconds()
float htfClose = request.security(syminfo.tickerid, htfTimeframeInput, close[1], lookahead = barmerge.lookahead_on)
float htfEma = request.security(syminfo.tickerid, htfTimeframeInput, ta.ema(close, htfEmaLenInput)[1], lookahead = barmerge.lookahead_on)
int htfDirRaw = htfClose > htfEma ? 1 : htfClose < htfEma ? -1 : 0
int htfDir = useMtfInput and htfValid and not na(htfClose) and not na(htfEma) ? htfDirRaw : 0
bool longGate = htfDir >= 0
bool shortGate = htfDir <= 0

// ————— Calculations: re-entry anchors (armed by the previous exit) —————
float anchorHigh = ta.highest(high, reentryWinInput * 2 + 1)
float anchorLow = ta.lowest(low, reentryWinInput * 2 + 1)
var float exitHighAnchor = na
var float exitLowAnchor = na
var bool reentryArmed = false
bool breakUpSrc = not na(exitHighAnchor) and (reentryModeInput == "Close" ? close > exitHighAnchor : high > exitHighAnchor)
bool breakDownSrc = not na(exitLowAnchor) and (reentryModeInput == "Close" ? close < exitLowAnchor : low < exitLowAnchor)
bool rlSignal = useReentryInput and reentryArmed and breakUpSrc and not breakDownSrc
bool rsSignal = useReentryInput and reentryArmed and breakDownSrc and not breakUpSrc

// ————— Calculations: signal governance (state machine + exclusivity + cooldown) —————
var int posState = 0
var int lastSignalBar = na
var string lastSignalText = "None"
bool signalBarKnown = not na(lastSignalBar)
bool cooldownOK = not signalBarKnown or bar_index - lastSignalBar > cooldownBarsInput

bool rawLongEntry = enableLongInput and (longSignalRaw or rlSignal) and posState <= 0 and cooldownOK and longGate
bool rawShortEntry = enableShortInput and (shortSignalRaw or rsSignal) and posState >= 0 and cooldownOK and shortGate
bool rawLongExit = posState == 1 and ((useReverseExitInput and shortCross) or (usePowerExitInput and powerDownFromOverbought) or (useBiasExitInput and biasDownZero))
bool rawShortExit = posState == -1 and ((useReverseExitInput and longCross) or (usePowerExitInput and powerUpFromOversold) or (useBiasExitInput and biasUpZero))

bool longExit = useExclusivityInput ? rawLongExit and not rawLongEntry and not rawShortEntry : rawLongExit
bool shortExit = useExclusivityInput ? rawShortExit and not rawLongEntry and not rawShortEntry : rawShortExit
bool longEntry = useExclusivityInput ? rawLongEntry and not longExit and not shortExit : rawLongEntry
bool shortEntry = useExclusivityInput ? rawShortEntry and not longExit and not shortExit : rawShortEntry

string entryKind = "Cross"
if longEntry
    entryKind := rlSignal ? "Re-entry" : (powerUpFromOversold and j > k ? "Power" : "Cross")
    posState := 1
    lastSignalBar := bar_index
    lastSignalText := "Long Entry (" + entryKind + ")"
    reentryArmed := false
else if shortEntry
    entryKind := rsSignal ? "Re-entry" : (powerDownFromOverbought and j < k ? "Power" : "Cross")
    posState := -1
    lastSignalBar := bar_index
    lastSignalText := "Short Entry (" + entryKind + ")"
    reentryArmed := false
else if longExit or shortExit
    posState := 0
    lastSignalBar := bar_index
    lastSignalText := longExit ? "Long Exit" : "Short Exit"
    if useReentryInput
        exitHighAnchor := anchorHigh
        exitLowAnchor := anchorLow
        reentryArmed := true

int cooldownLeft = na(lastSignalBar) ? 0 : math.max(0, cooldownBarsInput - (bar_index - lastSignalBar))

// ————— Calculations: ATR based stop / target (informational) —————
float atrValue = ta.atr(atrLenInput)
var float entryPrice = na
var int entryDir = 0
if longEntry
    entryPrice := close
    entryDir := 1
else if shortEntry
    entryPrice := close
    entryDir := -1
else if longExit or shortExit
    entryPrice := na
    entryDir := 0

float slPrice = useTpslInput and entryDir != 0 and not na(entryPrice) ? entryPrice - entryDir * slMultInput * atrValue : na
float tpPrice = useTpslInput and entryDir != 0 and not na(entryPrice) ? entryPrice + entryDir * tpMultInput * atrValue : na
plot(posState, "Position State (-1 short / 0 flat / 1 long)", display = display.data_window)
plot(slPrice, "Stop Loss (price)", display = display.data_window)
plot(tpPrice, "Take Profit (price)", display = display.data_window)
plot(atrValue, "ATR", display = display.data_window)

// ————— Visuals: legacy V1 markers (rollback switch) —————
plotshape(showLegacyPlotshapeInput and dianguancha == 10 ? 10 : na, title = "Legacy Bias Watch Bar", style = shape.square, location = location.absolute, color = C_WATCH, size = size.tiny)
plotshape(showLegacyPlotshapeInput and longSignalRaw ? j : na, title = "Legacy Long Marker", text = " Buy ", style = shape.labelup, location = location.absolute, color = C_LONG, textcolor = C_TEXT)
plotshape(showLegacyPlotshapeInput and shortSignalRaw ? j : na, title = "Legacy Short Marker", text = " Sell ", style = shape.labeldown, location = location.absolute, color = C_SHORT, textcolor = C_TEXT)

// ————— Visuals: signal labels —————
if showWatchMarkerInput and watchEdge
    label.new(x = bar_index, y = 0, text = "W", xloc = xloc.bar_index, yloc = yloc.price, color = C_WATCH, style = label.style_label_up, textcolor = C_TEXT, size = size.tiny)
if showLabelsInput
    if longEntry
        label.new(x = bar_index, y = j, text = rlSignal ? "RL" : "E", xloc = xloc.bar_index, yloc = yloc.price, color = rlSignal ? C_RE : C_LONG, style = label.style_label_up, textcolor = C_TEXT, size = size.small)
    if shortEntry
        label.new(x = bar_index, y = j, text = rsSignal ? "RS" : "S", xloc = xloc.bar_index, yloc = yloc.price, color = rsSignal ? C_RE_SHORT : C_SHORT, style = label.style_label_down, textcolor = C_TEXT, size = size.small)
    if longExit or shortExit
        label.new(x = bar_index, y = k, text = longExit ? "XL" : "XS", xloc = xloc.bar_index, yloc = yloc.price, color = C_EXIT, style = label.style_xcross, textcolor = C_TEXT, size = size.tiny)

color bgByState = posState > 0 ? C_BGLONG : posState < 0 ? C_BGSHORT : na
bgcolor(showBgStateInput ? bgByState : na, title = "Position State Background")

// ————— Visuals: dashboard —————
var table dash = table.new(position.top_right, 2, 8, bgcolor = color.new(color.black, 60), border_width = 1)
if showDashboardInput and barstate.islast
    string posText = posState == 1 ? "LONG" : posState == -1 ? "SHORT" : "FLAT"
    color posColor = posState == 1 ? C_LONG : posState == -1 ? C_SHORT : C_TEXT
    string htfText = htfDir == 1 ? "BULLISH" : htfDir == -1 ? "BEARISH" : "OFF"
    color htfColor = htfDir == 1 ? C_LONG : htfDir == -1 ? C_SHORT : C_TEXT
    string riskText = (na(slPrice) ? "n/a" : str.tostring(slPrice, format.mintick)) + " / " + (na(tpPrice) ? "n/a" : str.tostring(tpPrice, format.mintick))
    table.cell(dash, 0, 0, "Position", text_color = C_TEXT, text_size = size.tiny)
    table.cell(dash, 1, 0, posText, text_color = posColor, text_size = size.tiny)
    table.cell(dash, 0, 1, "Power (var26)", text_color = C_TEXT, text_size = size.tiny)
    table.cell(dash, 1, 1, str.tostring(var26, "#.##"), text_color = var26 < oversoldInput ? C_LONG : var26 > overboughtInput ? C_SHORT : C_TEXT, text_size = size.tiny)
    table.cell(dash, 0, 2, "Bias (mn)", text_color = C_TEXT, text_size = size.tiny)
    table.cell(dash, 1, 2, str.tostring(mn, "#.###") + (watchOn ? "  W" : ""), text_color = watchOn ? C_WATCH : C_TEXT, text_size = size.tiny)
    table.cell(dash, 0, 3, "ATR", text_color = C_TEXT, text_size = size.tiny)
    table.cell(dash, 1, 3, str.tostring(atrValue, format.mintick), text_color = C_TEXT, text_size = size.tiny)
    table.cell(dash, 0, 4, "Stop / Target", text_color = C_TEXT, text_size = size.tiny)
    table.cell(dash, 1, 4, riskText, text_color = C_TEXT, text_size = size.tiny)
    table.cell(dash, 0, 5, "HTF Gate", text_color = C_TEXT, text_size = size.tiny)
    table.cell(dash, 1, 5, htfText, text_color = htfColor, text_size = size.tiny)
    table.cell(dash, 0, 6, "Cooldown Left", text_color = C_TEXT, text_size = size.tiny)
    table.cell(dash, 1, 6, str.tostring(cooldownLeft) + " bars", text_color = C_TEXT, text_size = size.tiny)
    table.cell(dash, 0, 7, "Last Signal", text_color = C_TEXT, text_size = size.tiny)
    table.cell(dash, 1, 7, lastSignalText, text_color = C_TEXT, text_size = size.tiny)

// ————— Alerts: dynamic messages (close confirmed) —————
if longEntry
    alert("L1FSP long entry (" + entryKind + ") | power " + str.tostring(var26, "#.##") + " | bias " + str.tostring(mn, "#.###") + " | stop " + str.tostring(slPrice, format.mintick) + " | target " + str.tostring(tpPrice, format.mintick), alert.freq_once_per_bar_close)
if shortEntry
    alert("L1FSP short entry (" + entryKind + ") | power " + str.tostring(var26, "#.##") + " | bias " + str.tostring(mn, "#.###") + " | stop " + str.tostring(slPrice, format.mintick) + " | target " + str.tostring(tpPrice, format.mintick), alert.freq_once_per_bar_close)
if longExit
    alert("L1FSP long exit | power " + str.tostring(var26, "#.##") + " | bias " + str.tostring(mn, "#.###"), alert.freq_once_per_bar_close)
if shortExit
    alert("L1FSP short exit | power " + str.tostring(var26, "#.##") + " | bias " + str.tostring(mn, "#.###"), alert.freq_once_per_bar_close)

// ————— Alerts: alertcondition (global scope) —————
alertcondition(longEntry, title = "L1FSP Long Entry", message = "L1FSP: long entry signal confirmed")
alertcondition(shortEntry, title = "L1FSP Short Entry", message = "L1FSP: short entry signal confirmed")
alertcondition(longExit, title = "L1FSP Long Exit", message = "L1FSP: long position exit")
alertcondition(shortExit, title = "L1FSP Short Exit", message = "L1FSP: short position exit")
alertcondition(watchEdge, title = "L1FSP Bias Watch", message = "L1FSP: weighted bias entered the watch zone")
alertcondition(powerUpFromOversold, title = "L1FSP Power Up From Oversold", message = "L1FSP: power oscillator crossed up out of the oversold zone")
alertcondition(powerDownFromOverbought, title = "L1FSP Power Down From Overbought", message = "L1FSP: power oscillator crossed down out of the overbought zone")
